In this blog post, I want to show you a graph-based way to split up a class into several independent ones. We take a small example class from Michael Feathers' book "Working effectively with legacy code" and use Neo4j's Awesome Procedures On Cypher (APOC).
Hint: To run the notebook version of this blog post, you need to install the ipython-cypher extension.
class Reservation {
private int duration;
private int dailyRate;
private Date date;
private Customer customer;
private List fees = new ArrayList();
public Reservation(Customer customer, int duration, int dailyRate, Date date) {
this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}
public void extend(int additionalDays) {
duration += additionalDays;
}
public void extendForWeek() {
int weekRemainder = RentalCalendar.weekRemainderFor(date);
final int DAYS_PER_WEEK = 7;
extend(weekRemainder);
dailyRate = RateCalculator.computeWeekly(
customer.getRateCode()) / DAYS_PER_WEEK;
}
public void addFee(FeeRider rider) {
fees.add(rider);
}
int getAdditionalFees() {
int total = 0;
for (Iterator it = fees.iterator(); it.hasNext(); ) {
total += ((FeeRider) (it.next())).getAmount();
}
return total;
}
int getPrincipalFee() {
return dailyRate * RateCalculator.rateBase(customer) * duration;
}
public int getTotalFee() {
return getPrincipalFee() + getAdditionalFees();
}
}
In [62]:
%load_ext cypher
In [63]:
%%cypher
MATCH
()-[u:USES]->(),
(n:NewClass)-[s:SHOULD_DECLARE]->()
DELETE u,s,n
Out[63]:
In [64]:
%%cypher
MATCH
(c:Class {name : "Reservation"}),
(c)-[:DECLARES]->(m:Method),
(c)-[:DECLARES]->(f:Field),
(m)-[:READS|WRITES]->(f)
WHERE NOT (m:Constructor)
MERGE (m)-[u:USES]->(f)
RETURN m.name as method, type(u) as relType, f.name as field
Out[64]:
In [65]:
%%cypher
MATCH
(c:Class {name : "Reservation"}),
(c)-[:DECLARES]->(m:Method),
(c)-[:DECLARES]->(m2:Method),
(m)-[:INVOKES]->(m2:Method)
WHERE NOT (m:Constructor)
MERGE (m)-[u:USES]->(m2)
RETURN m.name as caller, type(u) as relType, m2.name as callee
Out[65]:
In [66]:
%%cypher
MATCH (m)-[u:USES]->(f)
WITH m, COUNT(u) as weight
SET m.weight = weight
RETURN m.name as method, weight
Out[66]:
In [67]:
%%cypher
MATCH (m)-[u:USES]->(f:Field)
WITH f, COUNT(u) as weight
SET f.weight = weight
RETURN f.name as field, weight
Out[67]:
In [68]:
%%cypher
MATCH (m)-[u:USES]->(m2:Method)
WITH m2, COUNT(u) as weight
SET m2.weight = weight
RETURN m2.name as callee, weight
Out[68]:
Now we have to move the information of the called items to the relationship.
In [69]:
%%cypher
MATCH (caller)-[r:USES]->(callee)
SET r.weight = callee.weight
RETURN count(r)
Out[69]:
In [70]:
%%cypher
CALL apoc.algo.community(25,null,'group','USES','OUTGOING','weight',10000)
Out[70]:
In [71]:
%%cypher
MATCH (m:Method)-[:USES]->(f:Field)<-[:USES]-(m2:Method)
WHERE m.group <> m2.group
WITH m.group as newGroupId, m2.group as oldGroupId
MATCH (n:Method) WHERE n.group = oldGroupId
SET n.group = newGroupId+oldGroupId
SET n.merged = true
RETURN DISTINCT(n.name), n.group;
Out[71]:
In [72]:
%%cypher
MATCH (m:Method)-[:USES]->(:Field)
WHERE NOT EXISTS(m.merged)
WITH m, m.group as groupId
SET m.merged = false
RETURN m.name, m.group;
Out[72]:
In [73]:
%%cypher
MATCH (m:Method)-[:USES]->(f)
WHERE (NOT EXISTS(m.merge) OR m.merge = False)
MERGE (c:NewClass { name: m.group})
MERGE (c)-[:SHOULD_DECLARE]->(m)
MERGE (c)-[:SHOULD_DECLARE]->(f)
RETURN c.name as newClass, m.name as method, f.name as field
Out[73]:
In [ ]: